package com.test.modules.token.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Random;

/**
 * 简单token管理类，不做存储，只通过加密完成时间的编码和解码，从而验证token的有效期（在这个实现中，token的有效期为5秒）。
 *
 * @author qd-hz
 */
@Component
@Slf4j
public class FileDownloadTokenManager {
    private static final String FILE_DOWNLOAD_TOKEN = "file_token";
    private long key;

    /**
     * 生成一个新token
     *
     * @return
     */
    public String createToken(int id) {
        long l = System.currentTimeMillis();

        return TokenEncoder.encode(l, id);
    }

    /**
     * 检查token是否有效。
     *
     * @param token
     */
    public void checkToken(String token, int id) {
        String checkFailMessage = "请求资源不存在";
        if (token == null) {
            log.warn("file token is null");
            throw new RuntimeException(checkFailMessage);
        }

        long timestamp = 0;
        try {
            timestamp = TokenEncoder.decode(token, id);
        } catch (RuntimeException e) {
            log.error("file token解析失败", e);
            throw new RuntimeException(checkFailMessage, e);
        }

        long currentTimeMillis = System.currentTimeMillis();
        // 有效期5秒
        long validTimestamp = timestamp + 5 * 1000;
        if (currentTimeMillis > validTimestamp || currentTimeMillis < timestamp) {
            log.error("file token has expired.");
            throw new RuntimeException(checkFailMessage);
        }
    }

    /**
     * 从map中获取file token的值，如果token有效，则无变化；否则，跑出RuntimeException.
     *
     * @param params
     * @param id
     */
    public void checkToken(Map<String, ?> params, int id) {
        String fileToken = (String)params.get(FILE_DOWNLOAD_TOKEN);
        checkToken(fileToken, id);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            long timestamp = System.currentTimeMillis();
            String token = TokenEncoder.encode(timestamp, 8);
            long decode = TokenEncoder.decode(token, 8);
            System.out.println("timestamp=" + timestamp +
                    ", token=" + token +
                    ", decode=" +  decode +
                    ", equals=" + (timestamp == decode));
        }
    }

    /**
     * 把long型数转换成字符串序列，结果值尽可能离散。
     * <br />
     * 结果字符串由2部分组成：编码串（n位，根据long数值的大小决定）+ 字符集数组起点位置标示（1位）
     * 优先定义字符集数组。
     */
    static class TokenEncoder {
        private static final char[] CHARSET = new char[]{
                'L', 'y', 'a', 'n', 'q', 'Q', 'h', '4', 'R', 'M',
                '1', 'W', 'c', 'k', '3', '0', 'D', 'S', '9', 'H',
                'r', 'd', '5', 'g', 'C', 'U', 'p', 'G', 'i', 'P',
                's', 'o', 't', '8', 'b', 'Y', 'N', 'e', '6', 'w',
                'u', 'K', 'x', 'Z', 'A', '2', 'J', 'E', 'v', 'f',
                'j', 'F', '7', 'X', 'B', 'l', 'z', 'm', 'T', 'V'
        };
        /** 余数模 */
        private static final int REMAINDER_MODE = 4;
        /** 混淆因子 */
        private static final long MIX_UP_FACTOR = 65535;
        /** ID位数 */
        private static final int ID_LENGTH = 2;

        public static String encode(long source, int id) {
            int mode = CHARSET.length;
            char[] buf = new char[32];
            int charPos = 32;

            int remainder = (int) (source % REMAINDER_MODE);
            int charsetBaseIndex = getCharsetBaseIndex(remainder);
            char charsetBaseChar = CHARSET[charsetBaseIndex];
            buf[--charPos] = charsetBaseChar;

            // 为id余留2位
            for (int i = 0; i < ID_LENGTH; i++) {
                int idIndex = getIndexOfLastPosition(id, mode, charsetBaseIndex);
                buf[--charPos] = CHARSET[idIndex];
                id = id / mode;
            }


            source = source ^ MIX_UP_FACTOR;

            while ((source / mode) > 0) {
                int ind = getIndexOfLastPosition(source, mode, charsetBaseIndex);
                buf[--charPos] = CHARSET[ind];
                source /= mode;
            }
            // 最左侧一位的位置
            int lastIndex = getIndexOfLastPosition(source, mode, charsetBaseIndex);
            buf[--charPos] = CHARSET[lastIndex];
            String str = new String(buf, charPos, (32 - charPos));
            return str;
        }

        /**
         * 解析字符串。解析失败则抛出RuntimeException异常。
         *
         * @param code
         * @param id
         * @return
         */
        public static long decode(String code, int id) {
            int mode = CHARSET.length;
            char chs[] = code.toCharArray();

            int maxIndex = chs.length - 1;
            char charsetBaseChar = chs[maxIndex--];
            int charsetBaseIndex = indexOfChar(charsetBaseChar);
            if (charsetBaseIndex == -1) {
                log.debug("字符集起始位置标示位字符是无效字符 - {}", charsetBaseChar);
                throw new RuntimeException("字符集起始位置错误");
            }

            // id
            long idLong = decodeInternal(chs, maxIndex, ID_LENGTH, charsetBaseIndex);
            if (idLong != id) {
                log.debug("解析出来的id与目标值不一致. parsed={}, target={}", idLong, id);
                throw new RuntimeException("id值不正确");
            }
            maxIndex -= ID_LENGTH;

            // timestamp
            long res = decodeInternal(chs, maxIndex, maxIndex + 1, charsetBaseIndex);;

            res = res ^ MIX_UP_FACTOR;

            int remainder = (int)(res % REMAINDER_MODE);
            int remainderCheck = charsetBaseIndex % REMAINDER_MODE;
            if (remainder != remainderCheck) {
                return -1;
            }
            return res;
        }

        /**
         * 从字符集的逻辑起点开始，当前位（最右面一位）所在的位置
         *
         * @param source
         * @param mode
         * @param charsetBaseIndex
         * @return
         */
        private static int getIndexOfLastPosition(long source, int mode, int charsetBaseIndex) {
            int ind = (int) ((source + charsetBaseIndex) % mode);
            return ind;
        }

        /**
         * 根据字符所在索引的值，计算字符表示的值
         *
         * @param index
         * @param mode
         * @param charsetBaseIndex
         * @return
         */
        private static int getValueFromIndex(int index, int mode, int charsetBaseIndex) {
            int value = (index + mode - charsetBaseIndex) % mode;
            return value;
        }

        /**
         * 获取字符集的起点位置
         */
        private static int getCharsetBaseIndex(int remainder) {
            int maxMixFactor = CHARSET.length / REMAINDER_MODE;
            Random random = new Random();
            int randomMixFactor = random.nextInt(maxMixFactor);
            int baseIndex = randomMixFactor * REMAINDER_MODE + remainder;

            return baseIndex;
        }

        /**
         * 将字符数组中指定区间的字符转换成数值.
         *
         * @param code
         * @param end 最后一个字符的下标
         * @param length 字符个数
         * @param charsetBaseIndex
         * @return
         */
        private static long decodeInternal(char[] code, int end, int length, int charsetBaseIndex) {
            int mode = CHARSET.length;

            long result = 0L;
            for (int i = end - length + 1; i <= end; i++) {
                int realValue = getRealValueForChar(code[i], charsetBaseIndex);

                result = result * mode + realValue;
                if (result < 0) {
                    log.debug("解析中间结果为负数");
                    throw new RuntimeException("解析中间结果为负数");
                }
            }
            return result;
        }


        private static int getRealValueForChar(char char1, int charsetBaseIndex) {
            int mode = CHARSET.length;

            int ind = indexOfChar(char1);
            if (ind == -1) {
                log.debug("无效字符");
                throw new RuntimeException("字符不在字符集中");
            }
            int realValue = getValueFromIndex(ind, mode, charsetBaseIndex);
            return realValue;
        }

        /**
         *
         * 查找字符在字符集数据组中的下标，没有找到则返回-1。
         *
         * @param ch
         * @return
         */
        private static int indexOfChar(char ch) {
            int mode = CHARSET.length;
            int ind = -1;
            for (int j = 0; j < mode; j++) {
                if (ch == CHARSET[j]) {
                    ind = j;
                    break;
                }
            }
            return ind;
        }
    }
}
